/*
 *      Copyright 2012-2013 Battams, Derek
 *       
 *       Licensed under the Apache License, Version 2.0 (the "License");
 *       you may not use this file except in compliance with the License.
 *       You may obtain a copy of the License at
 *
 *          http://www.apache.org/licenses/LICENSE-2.0
 *
 *       Unless required by applicable law or agreed to in writing, software
 *       distributed under the License is distributed on an "AS IS" BASIS,
 *       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *       See the License for the specific language governing permissions and
 *       limitations under the License.
 */
package org.schedulesdirect.api;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;

import org.apache.commons.io.IOUtils;
import org.schedulesdirect.api.exception.InvalidZipCodeException;

/**
 * An EpgClient provides access to Schedules Direct JSON feed data.
 * 
 * <p>An EpgClient instance must be used in order to gain access to all of the other
 * object types available in this API.  Only an EpgClient object can be directly
 * instantiated by classes outside of this package.</p>
 * 
 * @author Derek Battams &lt;derek@battams.ca&gt;
 *
 */
public abstract class EpgClient {
	/**
	 * The Schedules Direct server API version this client implements.
	 */
	static public final int API_VERSION = 20130709;
	
	/**
	 * Name of the zip file entry containing the processing server
	 */
	static public final String SERVER_ID_ENTRY = "serverID.txt";

	private String userAgent;
	
	/**
	 * Constructor
	 * @param userAgent The user agent to pass along to all SD HTTP requests
	 */
	public EpgClient(String userAgent) {
		this.userAgent = userAgent;
	}
	
	/**
	 * Provide all available headends for the given zip/postal code.
	 * 
	 * @param zip The zip/postal code to find headends for
	 * @param isoCountry ISO country code to search in; must be a supported country for the service (currently USA (US), Canada (CA), Brazil (BR))
	 * @return An array of Headend objects representing all available headends for the given zip; never returns null, but may return an empty array
	 * @throws InvalidZipCodeException Thrown if the given zip/postal code is invalid
	 * @throws IOException Thrown if there is any kind of IO error accessing the raw data feed
	 */
	public final Headend[] getHeadends(String zip, String isoCountry) throws InvalidZipCodeException, IOException {
		if(zip == null || !zip.toLowerCase().matches("\\d{5}|[a-z][0-9][a-z][0-9][a-z][0-9]|[a-z]{2}"))
			throw new InvalidZipCodeException(String.format("Zip is invalid! [%s]", zip.toUpperCase()));
		return getHeadendsForZip(String.format("PC:%s", (isoCountry.equals("CA") ? zip.substring(0, 4) : zip).toUpperCase()), isoCountry);
	}
	
	/**
	 * Provide all available headends for the given zip/postal code.
	 * 
	 * @param zip The zip/postal code to find headends for; this value is encoded for submission to Schedules Direct!
	 * @param isoCountry The ISO country code for the country to be searched; must be a country supported by the service (US, CA, BR at this time)
	 * @return An array of Headend objects representing all available headends for the given zip; never returns null, but may return an empty array
	 * @throws InvalidZipCodeException Thrown if the given zip/postal code is invalid
	 * @throws IOException Thrown if there is any kind of IO error accessing the raw data feed
	 */
	abstract protected Headend[] getHeadendsForZip(String zip, String isoCountry) throws InvalidZipCodeException, IOException;
	
	/**
	 * Provide all available headends for the logged in user.
	 * 
	 * <p>
	 * This method will only return those headends that are configured in the user's Schedules Direct account.
	 * These are the only headends to which the user is guaranteed to have access to listings data for.
	 * </p>
	 * @return An array of Headend objects, one for each lineup configured in the user's Schedules Direct account
	 * @throws IOException Thrown if there is any kind of IO error accessing the data feed
	 */
	abstract public Headend[] getHeadends() throws IOException;
	
	/**
	 * Get the headend for the given id
	 * @param id The id to retrieve the headend for; this is NOT a zip/postal code, but rather the unquiely assigned id for a headend
	 * @return The Headend instance for the given id or null if it could not be found
	 * @throws IOException Thrown if there is any kind of IO error accessing the raw data feed
	 */
	abstract public Headend getHeadendById(String id) throws IOException;
	
	/**
	 * Get the UserStatus object associated with this EpgClient connection
	 * @return The UserStatus object associated with this EpgClient instance
	 * @throws IOException Thrown if there is any kind of IO error accessing the raw data feed
	 */
	abstract public UserStatus getUserStatus() throws IOException;
	
	/**
	 * Set the user agent for all HTTP requests submitted to the SD servers from this client.
	 * 
	 * <p>A default agent string is generated if not set.</p>
	 * @param userAgent The user agent string to use for all HTTP requests to the SD servers
	 */
	public void setUserAgent(String userAgent) {
		this.userAgent = userAgent;
	}
	
	/**
	 * Return the current user agent string being submitted to the SD servers from this client.
	 * @return The current user agent string
	 */
	public String getUserAgent() {
		return String.format("%ssdjson/%s (%s Java %s)", userAgent != null && userAgent.length() > 0 ? userAgent + " " : "", Config.API_VERSION, System.getProperty("java.vendor"), System.getProperty("java.version"));
	}
	
	/**
	 * Close and free all resources associated with this client connection
	 * @throws IOException On any IO error
	 */
	abstract public void close() throws IOException;
	
	/**
	 * Fetch a single airing schedule for the given Station reference
	 * @param station The station to fetch the airing schedule for
	 * @return An array of Airing objects representing the airing schedule for the given Station
	 * @throws IOException Thrown on any IO error accessing the schedule data
	 */
	abstract protected Airing[] fetchSchedule(Station station) throws IOException;

	/**
	 * Fetch a Station's logo object
	 * @param station The station whose logo is to be fetched
	 * @return An InputStream containing the station's logo or null if one isn't available or an error occurred 
	 * @throws IOException Thrown on any IO error
	 */
	abstract protected InputStream fetchLogoStream(Station station) throws IOException;

	/**
	 * Write a station's logo to a file
	 * @param station The station whose logo is to be written to file
	 * @param dest The destination of the write
	 * @throws IOException Thrown on any IO error
	 */
	protected void writeLogoToFile(Station station, File dest) throws IOException {
		InputStream ins = fetchLogoStream(station);
		FileOutputStream outs = null;
		if(ins != null) {
			try {
				outs = new FileOutputStream(dest);
				IOUtils.copy(ins, outs);
			} finally {
				try { if(outs != null) outs.close(); } catch(IOException e) {}
				try { if(ins != null) ins.close(); } catch(IOException e) {}
			}
		}
	}
	
	/**
	 * Fetch a single Program object
	 * @param progId The program id to fetch
	 * @return The Program instance for the given program id
	 * @throws IOException Thrown on any IO error accessing the data
	 */
	abstract protected Program fetchProgram(String progId) throws IOException;
	
	/**
	 * Fetch multiple recording schedules in batch.
	 * 
	 * <p>
	 * This method is preferred to fetchSchedule() as it will grab
	 * multiple schedules much more efficiently.  Use this when	ever you can.
	 * </p>
	 * @param lineup The Lineup object to download schedules for
	 * @return A Map of Airing arrays, the key is the Station id, the value is that station's airing schedule
	 * @throws IOException In case of any IO error accessing the data
	 */
	abstract protected Map<Station, Airing[]> fetchSchedules(Lineup lineup) throws IOException;

	/**
	 * Fetch multiple programs in batch.
	 * 
	 * <p>
	 * This method is preferred to fetchProgram() as it will grab
	 * multiple programs much more efficiently.  Use this when ever you can.
	 * </p>
	 * @param progIds An array of program ids to fetch
	 * @return A Map of fetched programs; the key is the progId and the value is the Program instance
	 * @throws IOException On any IO error accessing the data
	 */
	abstract protected Map<String, Program> fetchPrograms(String[] progIds) throws IOException;
	
	/**
	 * Fetch all the lineups for the given Headend instance.
	 * @param headend The headend to fetch lineups for
	 * @return An array of Lineup instances; each entry is a lineup defined for the given Headend
	 * @throws IOException Thrown on any IO error accessing the data
	 */
	abstract protected Lineup[] fetchLineups(Headend headend) throws IOException;
	
	/**
	 * Purge the client's object cache.  This will force the client to reload all objects from
	 * their source.
	 */
	abstract public void purgeCache();
	
	/**
	 * Purge a specific object from the client's cache.  Accessing this object from this client
	 * after the purge will force the object to be reloaded from the client's upstream source.
	 * @param obj The object to purge from the client's local cache
	 */
	abstract public void purgeCache(Object obj);
	
	/**
	 * Acknowledge receipt of and delete a message object from the upstream provider.
	 * @param msg The message object to be deleted
	 * @throws IOException In case of any IO error upstream
	 */
	abstract public void deleteMessage(Message msg) throws IOException;
	
	/**
	 * Return details of the current state of the Schedules Direct system
	 * @return The current SD system status
	 * @throws IOException In case of any errors obtaining the status info
	 */
	abstract public SystemStatus getSystemStatus() throws IOException;
}
